import fs from 'node:fs';
import path from 'node:path';

import axios from 'axios';
import { pinyin } from 'pinyin-pro';

declare interface DefinitionBasic {
  type: 'integer' | 'string';
  title?: string;
  description?: string;
  format?: DefinitionType;
  example?: any;
}

declare interface DefinitionRef extends DefinitionBasic {
  $ref: string;
  originalRef: string;
}

declare interface DefinitionObject extends DefinitionBasic {
  type: 'object';
  required: string[];
  properties: Record<string, DefinitionType>;
}

declare interface DefinitionArray extends DefinitionBasic {
  type: 'array';
  uniqueItems?: boolean;
  items: DefinitionType;
}
declare type DefinitionType = DefinitionBasic | DefinitionRef | DefinitionObject;

declare interface Parameter {
  name: string;
  in: 'query' | 'body' | 'path';
  type?: string;
  description?: string;
  required?: boolean;
  schema?: DefinitionRef;
}
declare interface PathsItemData {
  tags: string[];
  summary: string;
  description: string;
  operationId: string;
  consumes: string[];
  produces: string[];
  security: Security[];
  'x-order': string;
  parameters?: Parameter[];
  responses: { [key: string]: DefinitionType };
}

declare type RequestMethod = 'post' | 'get' | 'delete' | 'put';

declare type Paths = Record<string, PathsItemData>;

declare interface Info {
  description: string;
  version: string;
  title: string;
  contact: Contact;
  license: License;
}

declare interface License {
  name: string;
}

declare interface Contact {
  name: string;
  url: string;
  email: string;
}

declare interface SwaggerData {
  swagger: string;
  info: Info;
  host: string;
  tags: { name: string; 'x-order': string }[];
  paths: Record<string, Paths>;
  securityDefinitions: SecurityDefinitions;
  definitions: { [key: string]: DefinitionType };
}

declare interface Swagger2JsOption {
  url: string;
  import: string;
  requestName: string;
  /** 存放的相对路径 */
  path: string;
}

export class Swagger2Js {
  /** @type {Swagger2JsOption} */
  option = {};

  /**
   * @param {Swagger2JsOption} option
   */
  constructor(option) {
    this.option = option;
  }

  //   触发生成代码入口
  async gen() {
    try {
      axios.interceptors.response.use((res) => {
        if (typeof res.data == 'string') {
          eval(`
        let json=${res.data};
        res.data= json;
        `);
        }
        return res;
      });
      const res = await axios(this.option.swagger);
      this.swagger = res.data;

      this.generatedefinition();
      this.generateFunction();
      const jsCodeStr = [...this.jsCode.values()].join('\n');
      const dtsTypeCodeStr = [...this.dtsTypeCode.values()].join('\n');
      const dtsFnCodeStr = [...this.dtsFnCode.values()].join('\n');

      const dirPath = path.resolve(this.option.path);
      fs.mkdirSync(dirPath, { recursive: true });

      const info = this.swagger?.info ?? {};
      const desc = JSON.stringify(info, 3, 3)
        .replaceAll(/[",{}]/g, '')
        .split('\n');
      const descStr = array2annotate(desc);
      fs.writeFileSync(
        path.resolve(dirPath, 'index.js'),
        `// GENERATED CODE - DO NOT MODIFY BY HAND\n\n${this.option.import}` + `\n${descStr}\n${jsCodeStr}`,
      );
      // eslint-disable-next-line no-console
      console.log('\u001B[36m%s\u001B[0m', `正在输出 ${path.resolve(dirPath, 'index.js')}`);
      fs.writeFileSync(
        path.resolve(dirPath, 'index.d.ts'),
        '// GENERATED CODE - DO NOT MODIFY BY HAND\n\n' + `\n${descStr}\n\n${dtsTypeCodeStr}\n${dtsFnCodeStr}`,
      );
      // eslint-disable-next-line no-console
      console.log('\u001B[36m%s\u001B[0m', `正在输出 ${path.resolve(dirPath, 'index.d.ts')}`);
    } catch (error) {
      // eslint-disable-next-line no-console
      console.log(error);
      // eslint-disable-next-line no-console
      console.log('\u001B[31m%s\u001B[0m', `生成 ${this.option.url} swagger文件 失败`);
    }
  }

  /**
   * d.ts类型描述代码
   * @type {Map<string,string>}
   */
  dtsTypeCode = new Map();

  /**
   * d.ts函数描述代码
   * @type {Map<string,string>}
   */
  dtsFnCode = new Map();

  /** @type {Map<string,string>} */
  jsCode = new Map();

  /** @type { SwaggerData } */
  swagger = {};

  /**
   * 生成单个dts描述代码
   * @param {DefinitionType} data
   */
  generateType(name, data = {}) {
    name = strConvert(name);
    let description = data.description;
    let type = 'any';
    switch (data.type) {
      case 'string':
        type = 'string';
        break;
      case 'integer':
        type = 'number';
        break;
      case 'array':
        (() => {
          const res = this.generateType(`${name}Array`, data.items);
          type = `${res.type}[]`;
        })();
        break;
      case 'object':
        (() => {
          type = name;
          const annotate = array2annotate([data.description || data.title]);
          const keys = Object.keys(data.properties ?? {});
          if (keys.length === 0) {
            const code = `\n${annotate}` + `\nexport declare type ${name} =  Record<string,any>;\n`;
            this.dtsTypeCode.set(name, code);
            return;
          }

          const requireds = data.required ?? [];
          const child = keys.map((key) => {
            const item = data.properties[key];
            const res = this.generateType(name + key, item);
            const required = requireds.includes(key);
            /** @type {string} */
            const annotations = [];
            const description = res.description || item.title;
            description && annotations.push(description);
            required && annotations.push('@requires');
            item.example !== undefined && annotations.push(`@example ${item.example}`);
            return {
              key,
              type: res.type,
              required,
              annotations,
            };
          });

          const code =
            `\n${annotate}` +
            `\nexport declare interface ${name} {` +
            `${child
              .map((e) => {
                const annotationStr = array2annotate(e.annotations);
                const code = `'${e.key}'${e.required ? '' : '?'}:${e.type};`;
                return `\n${annotationStr}\n${code}\n`;
              })
              .join('')}\n` +
            '\n}\n';

          this.dtsTypeCode.set(name, code);
        })();
        break;
      default:
        break;
    }
    if (data.originalRef) {
      type = strConvert(data.originalRef);
      description = data.originalRef;
    }

    return {
      type,
      description,
      example: data.example,
    };
  }

  /**
   * 生成所有dts
   */
  generatedefinition() {
    Object.keys(this.swagger.definitions).forEach((key) => {
      this.generateType(key, this.swagger.definitions[key]);
    });
  }

  /**
   * 生成函数
   */
  generateFunction() {
    const pathObjs = this.swagger.paths;
    return Object.keys(pathObjs).forEach((path) => {
      const pathData = pathObjs[path];
      const methods = Object.keys(pathData);
      methods.forEach((method) => {
        const requestObj = pathData[method];

        const upperPath = strConvert(path);

        const { summary, description, tags, parameters = [], responses } = requestObj;

        /** @type {string[]} */
        const annotations = [];
        tags?.length && annotations.push(`@tags ${tags}`);
        summary && annotations.push(`@summary ${summary}`);
        annotations.push(`@url ${path}`);
        description && annotations.push(`@description ${description}`);

        const queryParameters = parameters.filter((e) => e.in == 'query');
        const pathParameters = parameters.filter((e) => e.in == 'path');
        const bodyParameters = parameters.filter((e) => e.in == 'body');

        /** @type {DefinitionObject} */
        const paramDefinition = {
          type: 'object',
          description: `${summary} 请求参数`,
          required: [],
          properties: {},
        };
        queryParameters.filter((e) => e.required).length && paramDefinition.required.push('params');
        bodyParameters.filter((e) => e.required).length && paramDefinition.required.push('data');

        pathParameters.forEach((e) => {
          e.required && paramDefinition.required.push(e.name);
          paramDefinition.properties[e.name] = e;
        });

        if (queryParameters.length > 0) {
          /** @type {DefinitionObject} */
          const data = {
            type: 'object',
            description: `${summary} query参数`,
            properties: {},
          };
          queryParameters.forEach((e) => {
            data.properties[e.name] = e;
          });
          paramDefinition.properties.params = data;
        }

        if (bodyParameters.length > 0) {
          paramDefinition.properties.data = {
            ...bodyParameters[0],
            ...(bodyParameters[0].schema ?? {}),
          };
        }

        let paramType = 'any'; // 请求参数类型
        let paramRequired = false; // 请求参数必须性

        paramRequired = !!paramDefinition.required?.length;
        const paramRes = this.generateType(`${upperPath}Params`, paramDefinition);
        paramType = paramRes.type;

        const resultRes = this.generateType(`${upperPath}Result`, {
          ...Object.values(responses)?.[0],
          ...(Object.values(responses)?.[0]?.schema ?? {}),
        });
        const resultType = `Promise<${resultRes.type}>`;

        const requestName = `${strConvert(upperPath, 'lower')}by${strConvert(method)}`;

        const annotation = array2annotate(annotations);

        // js代码
        const js =
          `\n${annotation}` +
          `\nexport function ${requestName}(param) {
          return ${this.option.requestName}({
            url: \`${path.replaceAll('{', '${param?.')}\`,
            method:'${method}',
            ...param??{},
          });
        }`;
        this.jsCode.set(path + method, js);
        // dts代码
        const dts =
          `${annotation}` +
          `\nexport declare function ${requestName}(param${
            paramRequired ? '' : '?'
          }:Record<string, any> & ${paramType}):${resultType}\n\n`;
        this.dtsFnCode.set(path + method, dts);
      });
    });
  }
}

/**
 * 传入 注释文字列表  输入标准的注释字符串
 * @param {string[]} arr 注释列表
 */
function array2annotate(arr) {
  if (!arr?.length) {
    return '';
  }
  const content = arr.map((e) => {
    return ` * ${e}`;
  });
  return '/**' + `\n${content.join('\n')}` + '\n */';
}

/**
 * 字符串转为驼峰 并将中文转为拼音
 * @param {string } str 待转字符串
 * @param {'upper'|'lower'} format 大写驼峰还是小写驼峰
 */
function strConvert(str, format = 'upper') {
  if (!str) {
    return str;
  }
  const res = str.replaceAll(/(\W|_)(\w)?/g, ($1, $2, $3) => {
    const res = pinyin($2, {
      pattern: 'pinyin',
      toneType: 'none',
    });
    const py = res ?? '';
    let pyin = '';
    if (/\w+/.test(py)) {
      pyin = py[0].toUpperCase() + (py.slice(1) ?? '');
    }
    const other = ($3?.[0] ?? '')?.toUpperCase() ?? '';
    return `${pyin}${other}`;
  });
  if (format === 'lower') {
    return (res[0]?.toLowerCase() ?? '') + res.slice(1);
  } else {
    return (res[0]?.toUpperCase() ?? '') + res.slice(1);
  }
}

export default Swagger2Js;
